/* node.js core modules */
const fs = require('fs');
const pathUtil = require('path');
const crypto = require('crypto');
const {Url, URL} = require('url');

/* npm modules */
const deepMerge = require('deepmerge');
const isPlainObject = require('is-plain-object/index.cjs.js');
const {toSafeString, isString} = require('php-trim-plus');

const prop = (value) => ({configurable: false, writable: false, enumerable: true, value});

const isDir = (path) => {
	try {
		return fs.lstatSync(path).isDirectory();
	} catch (e) {
		return false;
	}
};

const md5 = (...segments) => {
	const str = segments.join('');
	if (str === '') return '';
	return crypto
		.createHash('md5')
		.update(str.toString())
		.digest('hex');
};

const arrayMerge = (target, source, options) => {
	const destination = target.slice();

	source.forEach((item, index) => {
		if (typeof destination[index] === 'undefined') {
			destination[index] = options.cloneUnlessOtherwiseSpecified(item, options);
		} else {
			if (options.isMergeableObject(item))
				destination[index] = deepMerge(target[index], item, options);
			else
				destination[index] = item;
		}
	});
	return destination;
};

const isArray = Array.isArray;

const DEFAULT_ENV_FILE = '.env';
const DEFAULT_CONFIG_DIR = 'config';
const DEFAULT_CONFIG_FILES = ['common.json'];
/**
 *
 * @type {{isAppendCustomConfig: boolean, envFile: *, configFiles: *, mergeOptions: {arrayMerge: *}, configDir: *}}
 */
const DEFAULT_OPTIONS = {
	envFile             : DEFAULT_ENV_FILE,
	configDir           : DEFAULT_CONFIG_DIR,
	isAppendCustomConfig: true,
	configFiles         : DEFAULT_CONFIG_FILES,
	mergeOptions        : {
		arrayMerge,
	},
};


const ENV_PROD = 'production';
const ENV_TEST = 'test';
const ENV_DEVEL = 'development';
const ENV_DEFAULT = ENV_PROD;

/**
 * @property {string} cwd 运行时根目录
 * @property {string} os 操作系统
 * @property {boolean} isWindows 是否 windows 操作系统
 * @property {string} userHome 操作系统的 user home
 * @property {string} root 应用程序的根目录
 * @property {string} hash 应用程序的 hash ，基于应用程序的根目录
 * @property {string} dirBasename 应用程序的目录名
 * @property {{isAppendCustomConfig: boolean, envFile: *, configFiles: *, mergeOptions: {arrayMerge: *}, configDir: *}} options 启动参数
 * @property {{env: string, config: string}} envVars 环境变量
 */
class AppCore {

	constructor(root, options) {
		const cwd = process.cwd();

		root = toSafeString(root);
		if (root === '') root = cwd;
		else {
			root = pathUtil.resolve(root);
			if (!isDir(root))
				throw new Error('app root does not exist!');
		}

		const baseName = pathUtil.basename(root);
		if (baseName === '')
			throw new Error('app root is not a valid directory!');

		const hash = md5(root);

		const os        = process.platform,
		      isWindows = os === 'win32',
		      userHome  = process.env[isWindows ? 'USERPROFILE' : 'HOME'];

		// 以下属性，不允许修改
		Object.defineProperties(this, {
			cwd        : prop(cwd),
			os         : prop(os),
			isWindows  : prop(isWindows), // 是否 windows 操作系统
			userHome   : prop(userHome), // 操作系统的 user home 目录
			hash       : prop(hash), // 应用程序的 hash
			root       : prop(root), // 项目（源代码）
			dirBasename: prop(baseName),
		});

		this._isBootstrap = false;
		this._isInitEnv = false;
		/**
		 * 应用启动配置
		 * @type {{isAppendCustomConfig: boolean, envFile: *, configFiles: *, mergeOptions: {arrayMerge: *}, configDir: *}}
		 */
		this.options = Object.assign({}, DEFAULT_OPTIONS);
		/**
		 * 环境变量
		 * @type {{env: *, config: null}}
		 */
		this.envVars = {
			env   : ENV_DEFAULT,
			config: null,
		};
		/**
		 * 配置内容
		 * @type {{}}
		 */
		this.config = {};

		if (typeof options !== 'undefined' && options !== null)
			this.setOptions(options);

		this.onConstructor();
	}

	onConstructor() {
	}

	/**
	 * 设置 App 启动配置
	 *
	 * @param {Object} options 启动配置
	 * @returns {this}
	 */
	setOptions(options) {
		if (!!this._isBootstrap) return this;
		Object.assign(this.options, options);
		return this;
	}

	/**
	 * 是否启动
	 *
	 * @returns {boolean}
	 */
	get isBootstrap() {
		return !!this._isBootstrap;
	}

	get isInitEnv() {
		return !!this._isInitEnv;
	}

	/**
	 *
	 * @returns {AppCore}
	 */
	initEnv() {
		if (!this._isInitEnv) {
			Object.defineProperties(this, {
				_isInitEnv: prop(true),
				envVars   : prop(this.loadEnv()),
			});
		}

		return this;
	}

	/**
	 *
	 * @param fn
	 * @returns {AppCore}
	 */
	bootstrap(fn) {
		if (!this._isInitEnv)
			this.initEnv();

		if (!this._isBootstrap) {
			Object.defineProperties(this, {
				_isBootstrap: prop(true),
			});

			this.config = this.loadConfig();

			if (typeof fn === 'function') {
				fn.call(this, this.env, this.config);
			}

			this.onBootstrap();
		}

		return this;
	}

	onBootstrap() {
	}

	get env() {
		return this.envVars.env;
	}

	getEnvFile() {
		const file = toSafeString(this.options.envFile) === '' ? DEFAULT_ENV_FILE : this.options.envFile;
		return pathUtil.isAbsolute(file) ? file : this.pathOf(file);
	}

	/**
	 * 加载 ENV 变量，这个方法只会根据 options 加载环境变量，但不会将变量写入当前对象
	 *
	 * @returns {{env: string, config: *}}
	 */
	loadEnv() {
		const data = this.loadFileAsObject(this.getEnvFile(), {
			env   : ENV_DEFAULT,
			config: null,
		});
		data.env = this.filterEnv(data.env);
		return data;
	}

	pathOf(...paths) {
		paths.unshift(this.root);
		return pathUtil.resolve(...paths);
	}

	filterEnv(value) {
		switch (toSafeString(value).toLowerCase()) {
			case ENV_DEVEL :
			case 'dev' :
			case 'devel' :
				return ENV_DEVEL;
			case ENV_TEST :
			case 'test' :
				return ENV_TEST;
			case ENV_PROD :
			case 'prod':
			default :
				return ENV_PROD;
		}
	}

	getConfigRoot() {
		let dir = toSafeString(this.options.configDir) === '' ? DEFAULT_CONFIG_DIR : this.options.configDir;
		if (!pathUtil.isAbsolute(dir))
			dir = this.pathOf(dir);
		return dir;
	}

	getConfigFiles() {
		const files = [];
		if (isArray(this.options.configFiles))
			files.push(...this.options.configFiles);

		if (this.isInitEnv) {
			files.push(`${this.env}.json`);
			if (!!this.options.isAppendCustomConfig) {
				if (isString(this.envVars.config))
					files.push(this.envVars.config);
				else if (isArray(this.envVars.config) && this.envVars.config.length > 0)
					this.envVars.config.forEach(item => files.push(item));
			}
		}

		return files;
	}

	loadConfig() {
		const dir = this.getConfigRoot();
		let data = Object.assign({}, this.config); // 复制一个克隆出来
		const mergeData = (file) => {
			data = this.loadFileAsObject(pathUtil.isAbsolute(file) ? file : this.pathOf(dir, file), data);
		};
		this.getConfigFiles().forEach(file => mergeData(file));
		return data;
	}

	convertPath(path) {
		if (typeof path === 'undefined' || path === null) return '';
		if (path instanceof URL) return path.toString();
		if (path instanceof Url) return path.href;
		if (typeof path === 'function') return this.convertPath(path());

		if (path instanceof Object) {
			if (isArray(path))
				path = pathUtil.join(path);
			else
				path = path.toString();
		} else {
			path = toSafeString(path);
		}
		if (!pathUtil.isAbsolute(path))
			path = this.pathOf(path);

		return path;
	}

	parseJsonAsObject(text, defaultData) {
		if (typeof text === 'function')
			return this.parseJsonAsObject(text(), defaultData);

		let isParse = true, json = {};
		if (typeof text === 'undefined' || text === null)
			isParse = false;

		if (text instanceof Object) {
			if (text instanceof Buffer)
				isParse = true;
			else
				json = text;
		}

		if (isParse) {
			try {
				json = JSON.parse(text);
			} catch (e) {
			}
			if (!isPlainObject(json))
				json = {};
		}

		if (isPlainObject(defaultData))
			json = this.merge(defaultData, json);

		return json;
	}

	loadFile(path, options) {
		let buffer, err;
		try {
			path = this.convertPath(path);
			buffer = fs.readFileSync(this.convertPath(path), options);
		} catch (e) {
			err = e;
		}
		return {path, err, buffer};
	}

	loadFileAsString(path) {
		const {buffer} = this.loadFile(path);
		return buffer ? buffer.toString().trim() : '';
	}

	loadFileAsObject(path, defaultData) {
		const {buffer} = this.loadFile(path);
		return this.parseJsonAsObject(buffer, defaultData);
	}

	merge(...items) {
		return deepMerge.all(items, this.options.mergeOptions);
	}
}

Object.defineProperties(AppCore, {
	PROD : prop(ENV_PROD),
	TEST : prop(ENV_TEST),
	DEVEL: prop(ENV_DEVEL),
});


module.exports = AppCore;